Volatile & 内存一致性模型
参考 What Volatile Means in Java,Memory Models,Consistency model。另外,stack overflow 的讨论或许有用。
Java 中的 volatile 可以保证可见性、有序性。可见性:线程 A 写入 volatile 变量,线程 B 读取该 volatile 变量可以读到最新值,并且在线程 A 写入 volatile 变量之前对 A 可见的变量值,在线程 B 读取该 volatile 变量之后对 B 也可见。有序性:禁止(编译器/CPU)对 volatile 变量相关的指令进行重排优化。虽然 Java 并发编程实战中提到 volatile 不能保证原子性,但是对 long/double 类型的 volatile 变量的简单赋值操作是原子的,我所说的简单赋值是指不依赖变量当前值的赋值操作。
问题
在《深入理解 Java 虚拟机》第 12 章中有提到 volatile 相关的汇编代码,简单的示例如下。书中提到 lock addl
相当于一个内存屏障,阻止跨内存屏障的指令重排,同时还会将当前处理器的缓存写入内存,以及使其他处理器的缓存失效,从而禁止指令重排。
1 | private static boolean a; |
1 | 0x000002271ed4378c: movabs $0x711dec880,%r10 ; {oop(a 'java/lang/Class'{0x0000000711dec880} = 'Test')} |
最初,我有个疑问,就是赋值语句之后的内存屏障无法禁止屏障之前的指令重排,例如此处的两个赋值语句 a=true
和 b=true
是否可能重排。询问 Jeremy(JSR-133 的作者之一)之后,他告诉我:
It is possible for processors to do that in general, but x86 doesn’t, so you don’t need a barrier there. Search for “total store order” if you’re curious.
然后,我查找和 TSO 相关的内容时,又一次找到 Russ Cox 的博客,以下内容部分来自该博客。
概念
顺序一致性(Sequential Consistency):单个处理器按照程序顺序执行(不重排指令),多个处理器按照某种顺序交错执行,这样的多处理器就是顺序一致的。
大多数指令集架构不提供顺序一致的内存模型,因为更强的一致性通常意味着更少的优化(更低的性能)。x86 使用 Total Store Order(TSO)内存模型:所有处理器都连接到单个共享内存,但是每个处理器有一个本地的写入队列,写入操作排队写入共享内存,读取操作会优先读取本地写入队列中的值(如果有的话)。ARM/POWER 的内存模型更加宽松:每个处理器从自己的内存完整副本中读取和写入,读取可以延迟到写入之后,并且每个写入都独立地传播到其他处理器,在写入传播时允许重新排序。
测试
Litmus Test(石蕊测试):初始时 x=0,y=0
(共享变量),rn
表示私有存储(例如,寄存器或者局部变量),不考虑编译器重排指令。
以下程序是否可以看到 r1=1,r2=0
?顺序一致性和 x86 中不可以,ARM/POWER 中可以。
1 | // Thread1 |
以下程序是否可以看到 r1=0,r2=0
?顺序一致性中不可以,x86 和 ARM/POWER 中可以。
1 | // Thread1 |
其他
区分 consistency 和 coherency:consistency 表示多个处理器对所有内存位置的操作的总顺序达成一致,coherency 表示多个处理器对相同内存位置的操作的总顺序达成一致。
JMM 为程序中所有操作定义了一个偏序关系,称为 Happens-Before。操作 A Happens-Before 操作 B 的含义是:如果操作 A 先于操作 B 发生,那么执行操作 B 的线程能够看到操作 A 的结果。如果两个操作没有 Happens-Before 关系,那么 JVM/CPU 可以对它们任意地重新排序。
Volatile & 内存一致性模型